package com.sitech.ibnms.c3p.command.shell;

import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.sitech.ibnms.c3p.command.CommandType;
import com.sitech.ibnms.c3p.command.script.ScriptCommand;
import com.sitech.ibnms.c3p.domain.C3pScriptParam;
import com.sitech.ibnms.c3p.domain.C3pScriptShellParam;
import com.sitech.ibnms.c3p.domain.ResourceAccount;
import com.sitech.ibnms.c3p.mappers.C3pScriptMapper;
import com.sitech.ibnms.c3p.mappers.ResourceAccountMapper;
import com.sitech.ibnms.c3p.util.C3pConstants;
import com.sitech.ibnms.c3p.util.JSONUtil;
import com.sitech.ismp.messageObject.cc.CccpResults;
import com.sitech.ismp.workflow.util.DES3;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.task.AsyncListenableTaskExecutor;
import org.springframework.util.concurrent.ListenableFuture;
import request.ScriptRequest;

import javax.annotation.Resource;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.sitech.ismp.messageObject.cc.CccpConstants.EXECUTE_FAILURE;
import static com.sitech.ismp.messageObject.cc.CccpConstants.EXECUTE_SUCCESS;
import static org.apache.commons.lang3.time.DateFormatUtils.format;

/**
 * Created by liujhc on 7/7.
 */
public class ShellCommand extends ScriptCommand {
    private static final Pattern PATTERN_OUT_PARAM = Pattern.compile("^(OUT_KPI|OUT_VAR):(.*)$");
    /**
     * 判断脚本中是否是 expect 命令访问执行主机
     */
    private static final Pattern PATTERN_EXPECT_TARGET_HOST = Pattern.compile("expect.*_TARGET_IP_ADDRESS");

    @Resource(name = "executor")
    private AsyncListenableTaskExecutor taskExecutor;
    @Autowired
    private C3pScriptMapper c3pScriptMapper;
    @Autowired
    private ResourceAccountMapper resourceAccountMapper;
    @Override
    public CommandType getType() {
        return CommandType.SHELL;
    }

    @Override
    public ListenableFuture<CccpResults> doExecuteScript(final ScriptRequest req, final ResourceAccount resourceAccount) {
        //是一个异步线程池 spring线程池
        return taskExecutor.submitListenable(new Callable<CccpResults>() {
            @Override
            public CccpResults call() throws Exception {
                CccpResults results = createResultsInstance(req, resourceAccount);
                results.setBeginTime(format(new Date(), DATE_TIME_FORMAT));
                try {
                    logger.info("******1doExecuteScript="+resourceAccount.getPort()+resourceAccount.getIpAddr()+resourceAccount.getUserName()+resourceAccount.getUnitId()+"resourceAccountMapper="+resourceAccountMapper);
                    if(resourceAccount.getPort()==0){
                        ResourceAccount _sa = resourceAccountMapper.findByIp(resourceAccount.getIpAddr());
                        logger.info("******_sa="+_sa.getPort()+_sa.getUnitId());
                        resourceAccount.setPort(_sa.getPort());
                        logger.info("******2doExecuteScript="+resourceAccount.getPort()+resourceAccount.getIpAddr()+resourceAccount.getUserName());

                    }
                    Map resultMap = executeShellRemotely(req, resourceAccount);

                    ShellExecutor.Result result = (ShellExecutor.Result)resultMap.get("RESULT");
                    String shellContent=resultMap.get("SHELL_CONTENT").toString();
                    results.getVariables().put(C3pConstants.SCRIPT_CONTENT,shellContent);//记录指令执行内容
                    String extUnitId=resourceAccount.getUnitId();
                    String execResult = new String(result.out);
                    String endTime = format(new Date(), DATE_TIME_FORMAT);
                    results.setEndTime(endTime);
                    results.setExecResult(execResult);
                    if(2==resourceAccount.getDisplayMode()){//判断为shell指令，且为二维展现方式
                        logger.info(":scriptId:::"+results.getParams().get(C3pConstants.PARAM_C3P_COMMAND_ID));
                        final List<C3pScriptParam> c3pScriptParams = ObjectUtils.defaultIfNull(
                                c3pScriptMapper.selectScriptParamByScriptId(results.getParams().get(C3pConstants.PARAM_C3P_COMMAND_ID)),
                                Collections.<C3pScriptParam>emptyList());
                        final List<C3pScriptShellParam> c3pScriptShellParams=new ArrayList<C3pScriptShellParam>(c3pScriptParams.size());//需要在report表生成的有指标数据
                        final List<Integer> keyColumns = new ArrayList<Integer>(c3pScriptParams.size());//记录选中的pk
                        for (C3pScriptParam c3pScriptParam : c3pScriptParams) {
                            if ("Y".equals(c3pScriptParam.getParamType())) {
                                keyColumns.add(Integer.valueOf(c3pScriptParam.getParamName()));
                            }
                            if(c3pScriptParam.getParamValue()!=null&&!c3pScriptParam.getParamValue().isEmpty()){
                                c3pScriptShellParams.add(new C3pScriptShellParam(c3pScriptParam));
                            }
                        }
                        logger.info("keyColumns:"+JSONUtil.toJSON(keyColumns));
                        int flagNum=0,maxArrLength=0,nowArrLength=0,rowNo=1,colNo=0;
                        List<String> shellRows=new ArrayList<String>();
                        shellRows.add("1");
                        for (String line : execResult.split("\n")) {
                            if("########".equals(line)){
                                flagNum++;
                                continue;
                            }
                            if(flagNum==1){
                                String [] lineArr=line.trim().replaceAll("  +"," ").split(" ");
                                nowArrLength=lineArr.length;
                                if(nowArrLength>maxArrLength){
                                    maxArrLength=nowArrLength;
                                }
                                List<String> bzInfo = new ArrayList<String>(keyColumns.size());
                                for (int keyColumn : keyColumns) {
                                    bzInfo.add(keyColumn<nowArrLength?lineArr[keyColumn]:"");
                                }
                                final String unitId = extUnitId + (bzInfo.isEmpty() ? "" : ":" + StringUtils.join(bzInfo, "_"));
                                for(C3pScriptShellParam c3pScriptShellParam:c3pScriptShellParams){
                                    colNo=c3pScriptShellParam.getParamName();
                                    logger.info("colNo"+colNo);
                                    final HashMap<String, String> kpi = new HashMap<String, String>();
                                    kpi.put("UNIT_ID", unitId);
                                    kpi.put("KPI_ID", c3pScriptShellParam.getParamValue());
                                    kpi.put("KPI_VALUE",colNo<nowArrLength?lineArr[colNo]:"");
                                    kpi.put("CLL_TIME", format(new Date(), DATE_TIME_FORMAT));
                                    kpi.put("EXT_UNIT_ID", extUnitId);
                                    kpi.put("POSITION", rowNo+":"+colNo);
                                    kpi.put("COL_NAME", colNo+"");
                                    kpi.put("DISPLAY_MODE","2");
                                    results.getKpis().add(kpi);
                                }
                                shellRows.add(StringUtils.join(lineArr,"\t"));
                                rowNo++;
                            }
                        }
                        String[] titleStrs=new String[maxArrLength];
                        for(int i=0;i<maxArrLength;i++){
                            titleStrs[i]=i+"";
                        }
                        shellRows.set(0,StringUtils.join(titleStrs,"\t"));
                        results.getVariables().put(C3pConstants.OUTVAR_SHELL_RESULT_CONTENT,StringUtils.join(shellRows,"\n"));
                    }else{
                        for (String line : execResult.split("\n")) {
                            Matcher matcher = PATTERN_OUT_PARAM.matcher(line);
                            if (matcher.matches()) {
                                String type = matcher.group(1);
                                String content = matcher.group(2).trim();
                                if ("OUT_KPI".equals(type)) {
                                    String[] split = content.split(" ", 3);
                                    if (split.length == 3) {
                                        HashMap<String, String> kpi = new HashMap<String, String>();
                                        kpi.put("UNIT_ID", split[0]);
                                        kpi.put("KPI_ID", split[1]);
                                        kpi.put("KPI_VALUE", split[2]);
                                        kpi.put("CLL_TIME", endTime);
                                        kpi.put("EXT_UNIT_ID", extUnitId);
                                        kpi.put("COL_NAME", "0");
                                        results.getKpis().add(kpi);
                                    }
                                } else if ("OUT_VAR".equals(type)) {
                                    String[] split = content.split("=", 2);
                                    if (split.length == 2) results.getVariables().put(split[0], split[1]);
                                }
                            }
                        }
                    }
                    if (result.status == 0) {
                        results.setExecFlag(EXECUTE_SUCCESS);
                    } else {
                        results.setExecFlag(EXECUTE_FAILURE);
                        results.setFailedReason(execResult);
                    }
                } catch (Throwable e) {
                    failed(results, e);
                }
                return results;
            }
        });
    }

    //远程执行shell
    private Map executeShellRemotely(final ScriptRequest req, final ResourceAccount resourceAccount) throws JSchException, IOException {
        String username = resourceAccount.getUserName();
        String password = resourceAccount.getPassword();
        String passwdDecrypt=DES3.decrypt(password);
        String hostname = resourceAccount.getIpAddr();
        int port  = resourceAccount.getPort();//ssh port
        String shellContent = processShellContent(req);
        String filename = req.getRequestId() + ".sh";
        scriptLogger.info("{}\n{}", filename, shellContent);
        // SHELL中使用expect登录目标主机时，也在执行器本地执行
        if (PATTERN_EXPECT_TARGET_HOST.matcher(shellContent).find()) {
            System.out.println("********************PATTERN_EXPECT_TARGET_HOST");
            logger.info("********************PATTERN_EXPECT_TARGET_HOST");
            username = null;
            password = null;
            hostname = "127.0.0.1";
            logger.info("using `expect` to connect _TARGET_IP_ADDRESS, {} will be executed locally", filename);
        }
        File file = writeToFile(filename, shellContent.getBytes());
        Session session = null;
        try {
            if (StringUtils.isNotEmpty(username)) {
            	try {
            		 session = new JSch().getSession(username, hostname);
                    logger.info("********** 1 session = new JSch().getSession(username, hostname) port = "+port);
                    System.out.println("********** 1 session = new JSch().getSession(username, hostname);");
            	}catch(Exception e ) {
                    logger.info("********** Exception 1 session = new JSch().getSession(username, hostname)"+username+","+hostname);
            		session = new JSch().getSession(username, hostname,port);
            	}
                if (session == null||!session.isConnected()) {
                    logger.info("**********2 session = new JSch().getSession(username, hostname);");
                    System.out.println("**********2 session = new JSch().getSession(username, hostname);");
                	session = new JSch().getSession(username, hostname,port);
                	if (session == null) {
                		throw new Exception("session is null");
                	}
                }
                if (StringUtils.isNotEmpty(password)) {
                    session.setPassword(passwdDecrypt);
                }
            } else {//没有username ，相当于 本地执行 以秘钥形式执行
                logger.info("**********8没有username ，相当于 本地执行 以秘钥形式执行");
                System.out.println("**********8没有username ，相当于 本地执行 以秘钥形式执行");
                JSch jSch = new JSch();
                session = jSch.getSession(hostname);
                jSch.addIdentity(System.getProperty("user.home") + "/.ssh/id_rsa");
            }
            try{
                logger.info("session="+session.toString());
                session.setConfig("StrictHostKeyChecking", "no");
                session.connect(30000);
                logger.info("========connect=");
                ShellExecutorImpl shellExecutor = new ShellExecutorImpl(session);
                logger.info("========shellExecutor=");
                String remoteFile = "/tmp/" + filename;
                shellExecutor.scpToRemote(file, filename);
                logger.info("========scpToRemote=");
                String command = String.format("chmod +x %1$s && %1$s", remoteFile);
                logger.debug("execute remote command: {}", command);
                ShellExecutor.Result result = shellExecutor.execRemote(command);
                logger.info("========ShellExecutor.Result result = shellExecutor.execRemote(command)=");

//            shellExecutor.execRemote("rm " + remoteFile);
                session.disconnect();
                session = null;
                scriptOutLogger.info("{}.sh.out\n{}", req.getRequestId(), result.toString());
                Map resultMap=new HashMap(2);
                resultMap.put("SHELL_CONTENT",shellContent.replaceAll("\""+passwdDecrypt+"\"","\"******\"").replaceAll("'"+passwdDecrypt+"'","'******'"));
                resultMap.put("RESULT",result);
                return resultMap;
            }catch (Exception e){
                logger.error("----------------addd-----------",e);
                throw new Exception("luanqibazao is null");
            }

        } catch (Exception e) {
            e.printStackTrace();
            logger.error("----------------mmm-----------",e);
            logger.error("Exception while executeShellRemotely", e);
            throw new RuntimeException();
        } finally {
            if (session != null) {
                session.disconnect();
            }
            if (file != null && !file.delete()) {
                logger.info("Shell content file[{}] delete failed", file.getPath());
            }
        }
    }

    /**
     * 处理脚本输入输出参数
     *
     * @return
     */
    private String processShellContent(final ScriptRequest req) {
        //noinspection unchecked
        Map<String, String> vars = JSONUtil.fromJSON(req.getInputVars(), Map.class);
        StringBuilder scriptContent = new StringBuilder();
        if (vars != null) {
            for (Map.Entry<String, String> in : vars.entrySet()) {
                scriptContent.append(in.getKey()).append("='").append(in.getValue()).append("'\n");
            }
        }

        // vars insert after shebang line
        String content = req.getContent();
        String[] strings = content.split("\n", 2);
        if (strings[0].matches("^\\s*#!/.*$")) {
            scriptContent.insert(0, "\n").insert(0, strings[0]);
            content = strings[1];
        }
        scriptContent.append(content).append("\n");
        content = scriptContent.toString().replace("#OUTPUT#", "echo OUT_KPI:");
        // 输出参数
        //noinspection unchecked
        vars = JSONUtil.fromJSON(req.getOutputVars(), Map.class);
        StringBuilder scriptVars = new StringBuilder();
        if (vars != null) {
            for (Map.Entry<String, String> out : vars.entrySet()) {
                String kpiId = out.getValue();
                if (StringUtils.isNotEmpty(kpiId)) {
                    content = content.replaceAll(String.format("#%s#", out.getKey()), kpiId);
                }
                scriptVars.append("\n").append("echo OUT_VAR: ").append(out.getKey()).append("=${").append(out.getKey()).append("}");
            }
        }
        return content + scriptVars.toString();
    }

    private File writeToFile(String filename, byte[] bytes) throws IOException {
        File tempFile = Files.createTempFile(filename, null).toFile();
        // 将生成的脚本输出到本地文件
        if (!tempFile.exists() && !tempFile.createNewFile()) {
            logger.warn("无法创建脚本文件:{}", tempFile.getPath());
        }
        FileOutputStream fos = new FileOutputStream(tempFile);
        try {
            fos.write(bytes);
        } catch (Exception e) {
            logger.error("文件[" + tempFile.getPath() + "]写入失败", e);
        } finally {
            fos.close();
        }
        return tempFile;
    }
}
